/*
 * Copyright 2009 Dean S. Jones
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.asmodaios.gwt.image.generator.rebind;

import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;

import com.asmodaios.gwt.client.ui.image.ImageKey;
import com.google.gwt.core.ext.Generator;
import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.TreeLogger.Type;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JMethod;
import com.google.gwt.core.ext.typeinfo.JParameter;
import com.google.gwt.core.ext.typeinfo.NotFoundException;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
import com.google.gwt.user.rebind.SourceWriter;

public class ImageBundleMapGenerator extends Generator
{
    private static final String MAP_QNAME                    = "java.util.Map";

    private static final String SET_QNAME                    = "java.util.Set";

    private static final String HASHMAP_QNAME                = "java.util.HashMap";

    private static final String COLLECTION_QNAME             = "java.util.Collection";

    private static final String COLLECTIONS_QNAME            = "java.util.Collections";

    private static final String GWT_QNAME                    = "com.google.gwt.core.client.GWT";

    private static final String IMAGEBUNDLEMAP_QNAME         = "com.asmodaios.gwt.client.ui.image.ImageBundleMap";

    private static final String IMAGEBUNDLE_QNAME            = "com.google.gwt.user.client.ui.ImageBundle";

    private static final String ABSTRACTIMAGEPROTOTYPE_QNAME = "com.google.gwt.user.client.ui.AbstractImagePrototype";

    @Override
    public String generate(TreeLogger logger, GeneratorContext context, String typeName) throws UnableToCompleteException
    {
        TypeOracle typeOracle = context.getTypeOracle();

        JClassType userType = this.getValidUserType(logger, typeName, typeOracle);

        return this.generateImplClass(logger, context, userType, typeOracle);
    }

    private JClassType getValidUserType(TreeLogger logger, String typeName, TypeOracle typeOracle) throws UnableToCompleteException
    {
        try
        {
            JClassType userType = typeOracle.getType(typeName);

            JClassType implType = typeOracle.findType(IMAGEBUNDLEMAP_QNAME);

            if (userType.isInterface() == null)
            {
                logger.log(TreeLogger.ERROR, userType.getQualifiedSourceName() + " must be an interface", null);

                throw new UnableToCompleteException();
            }
            if (userType.isAssignableTo(implType) == false)
            {
                logger.log(TreeLogger.ERROR, userType.getQualifiedSourceName() + " must be assignable to " + implType.getQualifiedSourceName(), null);

                throw new UnableToCompleteException();
            }
            return userType;
        }
        catch (NotFoundException e)
        {
            logger.log(TreeLogger.ERROR, "Unable to find required type(s)", e);

            throw new UnableToCompleteException();
        }
    }

    private String generateImplClass(TreeLogger logger, GeneratorContext context, JClassType userType, TypeOracle typeOracle) throws UnableToCompleteException
    {
        final JClassType abstractImagePrototype;

        try
        {
            abstractImagePrototype = typeOracle.getType(ABSTRACTIMAGEPROTOTYPE_QNAME);
        }
        catch (NotFoundException e)
        {
            logger.log(TreeLogger.ERROR, "GWT " + ABSTRACTIMAGEPROTOTYPE_QNAME + " class is not available", e);

            throw new UnableToCompleteException();
        }
        final JClassType imageBundleType;

        try
        {
            imageBundleType = typeOracle.getType(IMAGEBUNDLE_QNAME);
        }
        catch (NotFoundException e)
        {
            logger.log(TreeLogger.ERROR, "GWT " + IMAGEBUNDLE_QNAME + " class is not available", e);

            throw new UnableToCompleteException();
        }
        String subName = this.computeSubclassName(userType);

        String pkgName = userType.getPackage().getName();

        ClassSourceFileComposerFactory f = new ClassSourceFileComposerFactory(pkgName, subName);

        f.addImport(GWT_QNAME);

        f.addImport(MAP_QNAME);

        f.addImport(SET_QNAME);

        f.addImport(HASHMAP_QNAME);

        f.addImport(COLLECTION_QNAME);

        f.addImport(COLLECTIONS_QNAME);

        f.addImport(ABSTRACTIMAGEPROTOTYPE_QNAME);

        HashMap<JClassType, ArrayList<JMethod>> bundles = new HashMap<JClassType, ArrayList<JMethod>>();

        for (JClassType type : userType.getImplementedInterfaces())
        {
            if ((imageBundleType.isAssignableFrom(type)) && (type != userType))
            {
                ArrayList<JMethod> methods = new ArrayList<JMethod>();

                bundles.put(type, methods);

                for (JMethod method : type.getMethods())
                {
                    if (method.getReturnType() == abstractImagePrototype)
                    {
                        JParameter[] params = method.getParameters();

                        if ((null == params) || (0 == params.length))
                        {
                            methods.add(method);
                        }
                    }
                }
            }
            f.addImplementedInterface(type.getQualifiedSourceName());
        }
        f.addImplementedInterface(userType.getQualifiedSourceName());
        
        f.addImplementedInterface("Map<String, AbstractImagePrototype>");

        PrintWriter pw = context.tryCreate(logger, pkgName, subName);

        if (pw != null)
        {
            Set<String> ikeys = new HashSet<String>();
            
            Set<String> mkeys = new HashSet<String>();
            
            for (JClassType type : bundles.keySet())
            {
                for (JMethod method : bundles.get(type))
                {
                    mkeys.add(method.getName());
                }
            }
            SourceWriter sw = f.createSourceWriter(context, pw);

            sw.indent();

            for (JClassType type : bundles.keySet())
            {
                sw.print("private " + type.getQualifiedSourceName() + " " + this.computeBundleName(type) + " = GWT.create(" + type.getQualifiedSourceName() + ".class);");
            }
            sw.print("private Map<String, AbstractImagePrototype> m_bundles = null;");

            sw.println("public " + subName + "()");
            sw.println("{");
            sw.indent();
            sw.println("Map<String, AbstractImagePrototype> temp = new HashMap<String, AbstractImagePrototype>();");
            for (JClassType type : bundles.keySet())
            { 
                String bund = this.computeBundleName(type);

                for (JMethod method : bundles.get(type))
                {
                    String name = method.getName();

                    String ikey = this.getImageKey(type, method, ikeys, mkeys, logger);

                    sw.println("temp.put(\"" + ikey + "\", " + bund + "." + name + "());");
                }
            }
            sw.println("m_bundles = Collections.unmodifiableMap(temp);");
            sw.outdent();
            sw.println("}");
            sw.outdent();

            for (JClassType type : bundles.keySet())
            {
                for (JMethod method : bundles.get(type))
                {
                    String name = method.getName();
                    String bund = this.computeBundleName(type);

                    sw.indent();
                    sw.println("public AbstractImagePrototype " + name + "()");
                    sw.println("{");
                    sw.indent();
                    sw.println("return " + bund + "." + name + "();");
                    sw.outdent();
                    sw.println("}");
                    sw.outdent();
                }
            }
            sw.indent();
            sw.println("public boolean equals(Object o)");
            sw.println("{");
            sw.indent();
            sw.println("return m_bundles.equals(o);");
            sw.outdent();
            sw.println("}");
            sw.println("public int hashCode()");
            sw.println("{");
            sw.indent();
            sw.println("return m_bundles.hashCode();");
            sw.outdent();
            sw.println("}");
            sw.println("public String toString()");
            sw.println("{");
            sw.indent();
            sw.println("return m_bundles.toString();");
            sw.outdent();
            sw.println("}");
            sw.println("public void clear()");
            sw.println("{");
            sw.indent();
            sw.println("m_bundles.clear();");
            sw.outdent();
            sw.println("}");
            sw.println("public boolean containsKey(Object key)");
            sw.println("{");
            sw.indent();
            sw.println("return m_bundles.containsKey(key);");
            sw.outdent();
            sw.println("}");
            sw.println("public boolean containsValue(Object value)");
            sw.println("{");
            sw.indent();
            sw.println("return m_bundles.containsValue(value);");
            sw.outdent();
            sw.println("}");
            sw.println("public AbstractImagePrototype get(Object key)");
            sw.println("{");
            sw.indent();
            sw.println("return m_bundles.get(key);");
            sw.outdent();
            sw.println("}");
            sw.println("public boolean isEmpty()");
            sw.println("{");
            sw.indent();
            sw.println("return m_bundles.isEmpty();");
            sw.outdent();
            sw.println("}");
            sw.println("public Set<String> keySet()");
            sw.println("{");
            sw.indent();
            sw.println("return m_bundles.keySet();");
            sw.outdent();
            sw.println("}");
            sw.println("public AbstractImagePrototype put(String key, AbstractImagePrototype value)");
            sw.println("{");
            sw.indent();
            sw.println("return m_bundles.put(key, value);");
            sw.outdent();
            sw.println("}");
            sw.println("public void putAll(Map<? extends String, ? extends AbstractImagePrototype> t)");
            sw.println("{");
            sw.indent();
            sw.println("m_bundles.putAll(t);");
            sw.outdent();
            sw.println("}");
            sw.println("public AbstractImagePrototype remove(Object key)");
            sw.println("{");
            sw.indent();
            sw.println("return m_bundles.remove(key);");
            sw.outdent();
            sw.println("}");
            sw.println("public int size()");
            sw.println("{");
            sw.indent();
            sw.println("return m_bundles.size();");
            sw.outdent();
            sw.println("}");
            sw.println("public Collection<AbstractImagePrototype> values()");
            sw.println("{");
            sw.indent();
            sw.println("return m_bundles.values();");
            sw.outdent();
            sw.println("}");
            sw.println("public Set<java.util.Map.Entry<String, AbstractImagePrototype>> entrySet()");
            sw.println("{");
            sw.indent();
            sw.println("return m_bundles.entrySet();");
            sw.outdent();
            sw.println("}");
            sw.outdent();

            sw.commit(logger);
        }
        return f.getCreatedClassName();
    }

    private String getImageKey(JClassType type, JMethod method, Set<String> ikeys, Set<String> mkeys, TreeLogger logger)
    {
        String self = method.getName();

        ImageKey ikey = method.getAnnotation(ImageKey.class);

        if (null != ikey)
        {
            String name = ikey.value();

            if (null == name)
            {
                logger.log(Type.WARN, type.getQualifiedSourceName() + "." + self + "() has a null @ImageKey(), defaulting to: " + self);

                return self;
            }
            name = name.trim();

            if (name.length() < 1)
            {
                logger.log(Type.WARN, type.getQualifiedSourceName() + "." + self + "() has an empty @ImageKey(), defaulting to: " + self);

                return self;
            }
            if (mkeys.contains(name))
            {
                if (false == self.equals(name))
                {
                    logger.log(Type.WARN, type.getQualifiedSourceName() + "." + self + "() has an @ImageKey(" + name + ") that overrides a method name, defaulting to: " + self);
                }
                return self;
            }
            if (ikeys.contains(name))
            {
                logger.log(Type.WARN, type.getQualifiedSourceName() + "." + self + "() has an duplicate @ImageKey(" + name + "), defaulting to: " + self);

                return self;
            }
            ikeys.add(name);

            return name;
        }
        return self;
    }

    private String computeSubclassName(JClassType userType)
    {
        String baseName = userType.getName().replace('.', '_');

        return baseName + "_generatedImageBundleMap";
    }

    private String computeBundleName(JClassType userType)
    {
        String baseName = userType.getName().replace('.', '_');

        return "m_" + baseName;
    }
}
